--
--
--   _____       _ _     _                 _
--  | ___ \     (_) |   | |               | |
--  | |_/ /_   _ _| | __| |  ___ _   _ ___| |_ ___ _ __ ___
--  | ___ \ | | | | |/ _` | / __| | | / __| __/ _ \ '_ ` _ \
--  | |_/ / |_| | | | (_| | \__ \ |_| \__ \ ||  __/ | | | | |
--  \____/ \__,_|_|_|\__,_| |___/\__, |___/\__\___|_| |_| |_|
--                                __/ |
--                               |___/
--
--
inspect = require "meta/inspect"

-- silly little OS detection
local is_windows = package.config:sub(1,1) == "\\"
-- silly little architecture detection
local arch = ""
local arches = { -- todo: cover more possible values
    ["AMD64"] = "x64",
    ["x86_64"] = "x64",
    ["aarch64"] = "a64",
}
if is_windows then
    arch = os.getenv("PROCESSOR_ARCHITECTURE")
else
    arch = io.popen("uname -m"):read("*all")
end
arch = arch:match("%s*(%S*)%s*") -- info(walter): i'm assuming the arch text shouldn't contain whitespace
arch = arches[arch]

local options = {
    debug         = false,
    cuik          = false,
    tb            = false,
    driver        = false,
    shared        = false,
    gcc           = false,
    asan          = false,
    spall_auto    = false,
    no_threads    = false,
}

-- Cuik/TB are broken down into several pieces
local modules = {
    common = { srcs={"common/common.c", "common/perf.c", "common/emitter.c"} },

    -- libraries:
    --   C preprocessor
    cuik_pp = { srcs={
            "cuik_pp/lexer.c", "cuik_pp/cpp.c", "cuik_pp/diagnostic.c"
        }, flags="-DCONFIG_HAS_CUIKPP", deps={"common"}
    },
    --   C frontend
    cuik_c = { srcs={
            "cuik_c/libcuik.c",
            -- toolchain support
            "cuik_c/toolchains/msvc.c", "cuik_c/toolchains/gnu.c", "cuik_c/toolchains/darwin.c",
            -- architectures
            "cuik_c/targets/x64_desc.c",  "cuik_c/targets/aarch64_desc.c",
            "cuik_c/targets/mips_desc.c", "cuik_c/targets/wasm_desc.c",
        }, flags="-DCONFIG_HAS_CUIKC", deps={"common", "cuik_pp"}
    },
    --   Go frontend
    cuik_go = { srcs={
            "cuik_go/parser.c"
        }, flags="-DCONFIG_HAS_CUIKGO", deps={"common", "cuik_pp"}
    },
    --   TildeBackend
    tb = { srcs={
            "tb/libtb.c",
            -- archictectures
            "tb/x64/x64_target.c", "tb/aarch64/aarch64_target.c", "tb/mips/mips_target.c", "tb/wasm/wasm_target.c"
        }, flags="-DCONFIG_HAS_TB", deps={"common", "cuik_pp"}
    },
    --   Linker
    linker = { srcs={
            "linker/linker.c",
        }, flags="-DCONFIG_HAS_LINKER", deps={"common", "cuik_pp"}
    },
    -- executables:
    --   Cuik command line
    driver = { is_exe=true, srcs={"main/main_driver.c"}, deps={"common", "cuik_pp", "cuik_c", "tb", "linker"} },

    -- external dependencies
    mimalloc = { srcs={"mimalloc/src/static.c"}, flags="-DCONFIG_HAS_MIMALLOC" }
}

local lines = { "# AUTOGENERATED, GO EDIT THE BUILD.LUA" }
function rules(list)
    for i=1,#list do
        local name = list[i].name
        table.insert(lines, "rule "..name)
        for k,v in pairs(list[i]) do
            if k ~= "name" then
                table.insert(lines, "  "..k.." = "..v)
            end
        end
        table.insert(lines, "")
    end
end

function command(out, input, cmd, extra_input)
    if type(input) == "table" then
        input = table.concat(input, " ")
    end

    local line = "build "..out..": run "..input
    if extra_input ~= nil then
        line = line.." | "..extra_input
    end

    table.insert(lines, line)
    table.insert(lines, string.format("  cmd = %s", cmd:gsub("$in", input):gsub("$out", out)))
end

function build(name, cmd, input, content)
    if type(input) == "table" then
        input = table.concat(input, " ")
    end

    table.insert(lines, "build "..name..": "..cmd.." "..input)
    if content then
        local rows = {}
        for k,v in pairs(content) do
            table.insert(rows, "  "..k.." = "..v)
        end
        table.sort(rows)
        for i=1,#rows do
            table.insert(lines, rows[i])
        end
    end
end

function list_files(path, pat)
    local x = {}
    if is_windows then
        local cmd = io.popen("dir /B "..path:gsub("/", "\\")..pat)
        for c in cmd:lines() do
            x[#x + 1] = path..c
        end
        cmd:close()
    else
        local cmd = io.popen("find "..path..pat.." -maxdepth 1")
        for c in cmd:lines() do
            x[#x + 1] = c
        end
        cmd:close()
    end
    return x
end

function filename(file)
    return file:match("^.+/(.+)%..+")
end

local supported_archs = {
    x64 = "-DTB_HAS_X64",
    a64 = "-DTB_HAS_AARCH64",
}

local archs = ""
for i=1,#arg do
    if arg[i]:sub(1, 1) == "-" then
        local a = arg[i]:sub(2)
        if supported_archs[a] then
            archs = archs.." "..supported_archs[a]
            has_arch = true
        else
            options[a] = true
        end
    end
end

if not has_arch then
    print("Listen brosef, you gotta pass me an arch (or archs) to compile:")
    for k,v in pairs(supported_archs) do
        print(k)
    end
    os.exit(0)
end

local cc = "clang"
if options.gcc then
    cc = "gcc"
end

rules({
    { name = "cc",   command = cc.." $in $flags -MD -MF $out.d -o $out",   description = "CC $in", depfile = "$out.d" },
    { name = "cc2",  command = "cuik $in $flags -MD -MF $out.d -o $out",   description = "CC $in", depfile = "$out.d" },
    { name = "ld",   command = cc.." $in $flags -o $out",                  description = "LINK $out" },
    { name = "nasm", command = "nasm $in -f elf64 -o $out",                description = "NASM $out" },
    { name = "run",  command = "$cmd",                                     description = "$cmd"      },
    { name = "meta", command = arg[-1].." $script $out $in",               description = "META $out" }
})

local visited = {}
local srcs    = {}
local is_exe  = false

local cflags = "-c -g -I include -I common"
-- Warnings
cflags = cflags.." -Wall -Werror -Wno-unused -Wno-microsoft-enum-forward-reference -Wno-deprecated"

-- Options
if not options.debug then
    cflags = cflags.." -O2 -DNDEBUG"
end

if true then
    cflags = cflags.." -DMI_SKIP_COLLECT_ON_EXIT -I mimalloc/include"
end

if not options.no_threads then
    cflags = cflags.." -DCUIK_ALLOW_THREADS"
end

if options.asan then
    cflags = cflags.." -fsanitize=address"
end

if options.spall_auto then
    cflags = cflags.." -DCUIK_USE_SPALL_AUTO -finstrument-functions-after-inlining"
end
cflags = cflags..archs

-- Target-specific
if arch then
    cflags = cflags.." -march=haswell"
end

local ldflags = ""
if is_windows then
    cflags  = cflags.." -D_CRT_SECURE_NO_WARNINGS"
    ldflags = "-g"
    if options.shared then
        cflags = cflags.." -DCUIK_DLL -DTB_DLL"
        ldflags = ldflags.." -shared"
    end

    if not options.gcc then
        ldflags = ldflags.." -fuse-ld=lld-link"
    end
else
    cflags  = cflags.." -D_GNU_SOURCE"
    ldflags = " -lc -lm -g"
    if not options.gcc then
        ldflags = ldflags.." -fuse-ld=lld"
    end

    if options.shared then
        cflags = cflags.." -fPIC"
        ldflags = ldflags.." -shared"
    end
end

local function walk(name)
    if visited[name] then
        return
    end
    visited[name] = true

    local m = modules[name]
    -- process deps first
    if m.deps then
        for i=1,#m.deps do walk(m.deps[i]) end
    end
    -- accumulate CFLAGS
    if m.flags then
        cflags = cflags.." "..m.flags
    end
    if m.is_exe then
        is_exe = true
    end
    -- process self
    for i=1,#m.srcs do
        table.insert(srcs, m.srcs[i])
    end
end

walk("mimalloc")

if options.cuik then
    walk("cuik_c")
end

-- whatever the options says to compile, do that
for k,v in pairs(options) do
    if v and modules[k] then walk(k) end
end

-- Metaprogram: DSL's node stuff generator
if visited["cuik_pp"] then
    command("cuik_pp/keywords.h cuik_pp/dfa.h", "meta/lexgen.lua", arg[-1].. " $in", "")
end

-- Metaprogram: Compile the freestanding headers
if visited["cuik_c"] then
    local headers = list_files("freestanding/", "*.h")
    local out = "bin/objs/freestanding.c"

    table.insert(lines, string.format("build %s: meta %s | meta/hexembed.lua", out, table.concat(headers, " ")))
    table.insert(lines, "    script = meta/hexembed.lua")
    table.insert(srcs, out)
end

-- Metaprogram: DSL's node stuff generator
if visited["tb"] then
    command("tb/tb_gen_private.h include/tb_gen_public.h", "meta/new_dsl.lua", arg[-1].." $in tb/tb.dsl tb/tb_gen_private.h include/tb_gen_public.h \""..archs.."\"", "tb/tb.dsl tb/x64/x64.dsl meta/arch_isel.lua")
end

-- Normal C source
local objs = {}
table.insert(lines, "")
table.insert(lines, "flags = "..cflags)
for i,f in ipairs(srcs) do
    local out = "bin/objs/"..filename(f)..".o"
    local extra = ""
    if f == "cuik_pp/lexer.c" then
        extra = " | cuik_pp/dfa.h"
    end

    table.insert(lines, string.format("build %s: cc %s%s", out, f, extra))
    table.insert(objs, out)
end
table.insert(lines, "")

local out = "bin/cuik"
if is_exe then
    if options.shared then
        out = out..".dll"
    else
        out = out..".exe"
    end
elseif options.shared then
    out = out..".so"
end

table.insert(lines, string.format("build %s: ld %s", out, table.concat(objs, " ")))
table.insert(lines, "    flags = "..ldflags)
table.insert(lines, "")

-- Export & run ninja file
ninja = io.open("build.ninja", "wb")
ninja:write(table.concat(lines, "\n"))
ninja:close()
local _0, _1, res = os.execute("ninja")
if res and res ~= 0 then os.exit(res) end
